package hyperliquid

import (
	"context"
	"fmt"
	"math"
	"strconv"
	"strings"
	"time"
)

// GetAssetIndex resolves the exchange asset index for the given coin.
func (c *Client) GetAssetIndex(ctx context.Context, coin string) (int, error) {
	key := canonicalAssetKey(coin)
	if key == "" {
		return 0, fmt.Errorf("hyperliquid: empty coin symbol")
	}

	_ = c.maybeRefreshAssetDirectory(ctx)

	if idx, ok := c.cachedAssetIndex(key); ok {
		return idx, nil
	}
	if err := c.refreshAssetDirectory(ctx); err != nil {
		return 0, err
	}
	if idx, ok := c.cachedAssetIndex(key); ok {
		return idx, nil
	}
	return 0, fmt.Errorf("hyperliquid: asset %s not found", coin)
}

// GetAssetInfo returns cached asset metadata.
func (c *Client) GetAssetInfo(ctx context.Context, coin string) (*AssetInfo, error) {
	key := canonicalAssetKey(coin)
	if key == "" {
		return nil, fmt.Errorf("hyperliquid: empty coin symbol")
	}
	_ = c.maybeRefreshAssetDirectory(ctx)
	if info, ok := c.cachedAssetInfo(key); ok {
		return &info, nil
	}
	if err := c.refreshAssetDirectory(ctx); err != nil {
		return nil, err
	}
	if info, ok := c.cachedAssetInfo(key); ok {
		return &info, nil
	}
	return nil, fmt.Errorf("hyperliquid: asset info %s not found", coin)
}

func (c *Client) cachedAssetIndex(key string) (int, bool) {
	c.assetMu.RLock()
	defer c.assetMu.RUnlock()
	idx, ok := c.assetIndex[key]
	return idx, ok
}

func (c *Client) cachedAssetInfo(key string) (AssetInfo, bool) {
	c.assetMu.RLock()
	defer c.assetMu.RUnlock()
	info, ok := c.assetInfo[key]
	return info, ok
}

func (c *Client) refreshAssetDirectory(ctx context.Context) error {
	var resp MetaAndAssetCtxsResponse
	if err := c.doInfoRequest(ctx, InfoRequest{Type: "metaAndAssetCtxs"}, &resp); err != nil {
		return err
	}
	if len(resp.Universe) == 0 {
		return fmt.Errorf("hyperliquid: metaAndAssetCtxs response contained no assets")
	}

	index := make(map[string]int, len(resp.Universe))
	info := make(map[string]AssetInfo, len(resp.Universe))
	for idx, entry := range resp.Universe {
		key := canonicalAssetKey(entry.Name)
		if key == "" {
			continue
		}
		var ctx AssetCtx
		if idx < len(resp.AssetCtxs) {
			ctx = resp.AssetCtxs[idx]
		}
		info[key] = AssetInfo{
			Name:         entry.Name,
			SzDecimals:   entry.SzDecimals,
			MaxLeverage:  entry.MaxLeverage,
			MarginTable:  entry.MarginTable,
			OnlyIsolated: entry.OnlyIsolated,
			IsDelisted:   entry.IsDelisted,
			Index:        idx,
			MarkPx:       ctx.MarkPx,
			MidPx:        ctx.MidPx,
			OraclePx:     ctx.OraclePx,
			ImpactPxs:    append([]string(nil), ctx.ImpactPxs...),
		}
		index[key] = idx
	}

	c.assetMu.Lock()
	c.assetIndex = index
	c.assetInfo = info
	if c.clock != nil {
		c.assetLastRef = c.clock()
	} else {
		c.assetLastRef = time.Now()
	}
	c.assetMu.Unlock()
	return nil
}

func canonicalAssetKey(symbol string) string {
	return strings.ToUpper(strings.TrimSpace(symbol))
}

// maybeRefreshAssetDirectory refreshes the asset directory if the TTL has
// expired. It swallows errors to keep call sites simple; the normal lookup
// path will attempt a hard refresh if needed.
func (c *Client) maybeRefreshAssetDirectory(ctx context.Context) error {
	if c.assetTTL <= 0 {
		return nil
	}
	now := time.Now
	if c.clock != nil {
		now = c.clock
	}
	c.assetMu.RLock()
	last := c.assetLastRef
	c.assetMu.RUnlock()
	if last.IsZero() || now().Sub(last) > c.assetTTL {
		return c.refreshAssetDirectory(ctx)
	}
	return nil
}

// RoundPriceToSigFigs rounds the price to the specified number of significant
// figures. It returns a decimal string with trimmed trailing zeros.
func RoundPriceToSigFigs(price float64, sigfigs int) string {
	if sigfigs <= 0 || !isFinite(price) || price == 0 {
		return "0"
	}
	sign := 1.0
	if price < 0 {
		sign = -1
		price = -price
	}
	exp := math.Floor(math.Log10(price))
	scale := math.Pow(10, float64(sigfigs)-1-exp)
	rounded := sign * math.Round(price*scale) / scale
	if rounded == 0 || !isFinite(rounded) {
		return "0"
	}
	absr := rounded
	if absr < 0 {
		absr = -absr
	}
	// decimals required to preserve sigfigs in fixed notation
	decExp := math.Floor(math.Log10(absr))
	decimals := int(math.Max(0, float64(sigfigs-1)-decExp))
	s := strconv.FormatFloat(rounded, 'f', decimals, 64)
	if decimals > 0 {
		// Only trim trailing zeros when fractional digits are present. Hyperliquid strictly validates
		// the magnitude we submit; previously we stripped zeros from whole-number prices
		// (e.g. 110820 -> 11082) which turned a ~110k quote into a ~11k quote and the exchange
		// would reject the payload with HTTP 422 "Failed to deserialize the JSON body into the target type".
		s = strings.TrimRight(s, "0")
		s = strings.TrimRight(s, ".")
	}
	if s == "" || s == "-" {
		return "0"
	}
	return s
}

func isFinite(f float64) bool { return !math.IsNaN(f) && !math.IsInf(f, 0) }

// FormatPrice rounds a raw price to the client's configured significant figures
// and returns a trimmed decimal string. It optionally checks the asset exists
// to surface early errors when an unknown symbol is provided.
func (c *Client) FormatPrice(ctx context.Context, coin string, price float64) (string, error) {
	if price <= 0 || !isFinite(price) {
		return "0", fmt.Errorf("hyperliquid: invalid price")
	}
	// Ensure asset directory is primed so callers get early symbol errors.
	if _, err := c.GetAssetInfo(ctx, coin); err != nil {
		return "", err
	}
	if c.priceSigFigs <= 0 {
		c.priceSigFigs = 5
	}
	return RoundPriceToSigFigs(price, c.priceSigFigs), nil
}
